package cn.wokoba.trigger.tech.aop;

import cn.wokoba.trigger.tech.AccessInterceptor;
import cn.wokoba.trigger.tech.fallstrategy.FallbackStrategy;
import com.google.common.cache.Cache;
import com.google.common.cache.CacheBuilder;
import com.google.common.reflect.TypeToken;
import com.google.common.util.concurrent.RateLimiter;
import io.micrometer.common.util.StringUtils;
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.springframework.stereotype.Component;

import java.lang.reflect.Field;
import java.util.concurrent.TimeUnit;

@Slf4j
@Aspect
@Component
public class RateLimiterAOP {
    private final Cache<String, RateLimiter> askCache = CacheBuilder.newBuilder()
            .expireAfterWrite(5, TimeUnit.SECONDS)
            .build();
    private final Cache<String, Long> blacklist = CacheBuilder.newBuilder()
            .expireAfterWrite(30, TimeUnit.MINUTES)
            .build();

    @Pointcut("@annotation(cn.wokoba.trigger.tech.AccessInterceptor)")
    public void aopPoint() {
    }

    @Around("aopPoint() && @annotation(accessInterceptor)")
    public Object doAop(ProceedingJoinPoint joinPoint, AccessInterceptor accessInterceptor) throws Throwable {
        String key = accessInterceptor.key();
        String keyattk = null;
        if (StringUtils.isBlank(key)) {
            return joinPoint.proceed();
        }
        Object[] args = joinPoint.getArgs();
        if (args[0] instanceof String) keyattk = args[0].toString();
        for (Object arg : args) {
            try {
                if (StringUtils.isNotBlank(keyattk)) break;
                Field field;
                try {
                    field = TypeToken.of(arg.getClass()).getRawType().getDeclaredField(key);
                } catch (NoSuchFieldException e) {
                    field = TypeToken.of(arg.getClass()).getRawType().getSuperclass().getDeclaredField(key);
                }
                field.setAccessible(true);
                keyattk = (String) field.get(arg);
                field.setAccessible(false);
            } catch (IllegalAccessException e) {
                log.error("获取路由属性值失败 attr：{}", key, e);
            }
        }

        log.info("aop attr {}", keyattk);
        if (accessInterceptor.blacklistCount() != 0
                && null != blacklist.getIfPresent(keyattk)
                && blacklist.getIfPresent(keyattk) > accessInterceptor.blacklistCount()) {
            log.info("限流-黑名单拦截(24h)：{}", keyattk);
            return executeFallbackStrategy(joinPoint, accessInterceptor.fallbackMethod());
        }
        RateLimiter rateLimiter = askCache.getIfPresent(keyattk);
        if (rateLimiter == null) {
            rateLimiter = RateLimiter.create(accessInterceptor.permitsPerSecond());
            askCache.put(keyattk, rateLimiter);
        }

        if (!rateLimiter.tryAcquire(accessInterceptor.timeout(), TimeUnit.SECONDS)) {
            Long count = blacklist.getIfPresent(keyattk);
            if (null == count) {
                blacklist.put(keyattk, 1L);
            } else blacklist.put(keyattk, count + 1L);
            log.info("限流-超频次拦截：{}", keyattk);
            return executeFallbackStrategy(joinPoint, accessInterceptor.fallbackMethod());
        }
        return joinPoint.proceed();
    }

    private Object executeFallbackStrategy(ProceedingJoinPoint joinPoint, Class<? extends FallbackStrategy> fallbackStrategyClass) throws Throwable {
        FallbackStrategy strategy = fallbackStrategyClass.getDeclaredConstructor().newInstance();
        return strategy.handleFallback(joinPoint);
    }


}
